package CI;
import java.util.ArrayList;

public class Evaluator {
	public static Object meval(Object expr, Environment env) throws EvalError {
		Object returnval;
		if (isPrimitive(expr)) {
			returnval = evalPrimative(expr);
		}
		else if (isIf(expr)) {
			returnval = evalIf(expr,env);
		}
		else if (isCond(expr)) {
			returnval = evalCond(expr,env);
		}
		else if (isDefinition(expr)) {			
			evalDefinition(expr, env);
			returnval =  "";
		}
		else if (isName(expr)) {		
			returnval = evalName(expr,env);
		}
		else if (isLambda(expr)) {
			returnval = evalLambda(expr,env);
		}
		else if (isApplication(expr)) {
			returnval = evalApplication(expr, env);
		}
		else {
			throw new EvalError("Unknown expression type: " + expr.toString());
		}
		return returnval;
	}
	private static boolean isPrimitive(Object expr) {
		return (isNumber(expr) || isPrimitiveProcedure(expr) || isNull(expr));
	}
	
	private static boolean isNumber(Object expr) {	
		if (expr instanceof String) {
			String expr_s = (String)expr;
			if (expr_s.length() < 1) return false;
			if (Character.isDigit(expr_s.charAt(0))) {
				return true;
			}
			if (expr_s.length() < 2) return false;
			if (expr_s.charAt(0)=='-') {				
				if (Character.isDigit(expr_s.charAt(1))) {
					return true;
				}	
			}
		}
		return false;
	}
	private static boolean isNull(Object expr) {
		if (expr instanceof String) {
			String expr_s = (String)expr;
			return expr_s.equals("null");
		}
		return false;
	}
	private static boolean isPrimitiveProcedure(Object expr) {
		if (expr instanceof String) {
			String nexpr = (String) expr;
			if (nexpr.equals("+")) 
				return true;			
			else if (nexpr.equals("*"))
				return true;
			else if (nexpr.equals("-"))
				return true;
			else if (nexpr.equals("="))
				return true;
			else if (nexpr.equals("zero?"))
				return true;
			else if (nexpr.equals("*"))
				return true;		
			else if (nexpr.equals("<"))
				return true;
			else if (nexpr.equals(">"))
				return true;		
			else if (nexpr.equals("<="))
				return true;			
			else if (nexpr.equals("cons"))
				return true;
			else if (nexpr.equals("car"))
				return true;		
			else if (nexpr.equals("cdr"))
				return true;		
			else if (nexpr.equals("null?"))
				return true;
			else if (nexpr.equals("list"))
				return true;	
		}
		return false;
	}
	
	@SuppressWarnings("unchecked")
	public static boolean isSpecialForm(Object expr, String keyword) {
		if (expr instanceof ArrayList<?>) {			
			ArrayList<Object>nexpr = (ArrayList<Object>) expr;
			if (nexpr.size() > 0) {
				if (nexpr.get(0).equals(keyword)) {
					return true;
				}
			}
		}
		return false;
	}

	public static boolean isIf(Object expr) {
		return isSpecialForm(expr, "if");
	}
	public static boolean isCond(Object expr) {
		return isSpecialForm(expr, "cond");
	}
	public static boolean isLambda(Object expr) {
		return isSpecialForm(expr, "lambda");
	}
	public static boolean isDefinition(Object expr) {		
		return isSpecialForm(expr, "define");
	}
	public static boolean isApplication(Object expr) {
		return (expr instanceof ArrayList<?>);
	}
	public static boolean isName(Object expr) {
		return (expr instanceof String);
	}
	public static Object evalPrimative(Object expr) {
		if (isNumber(expr)) {
			return Integer.parseInt(((String) expr));
		}
		else if (isNull(expr)) {
			return null;
		}
		else {
			return expr;
		}
	}
	@SuppressWarnings("unchecked")
	public static Object evalIf(Object expr, Environment env) throws EvalError {
		assert(isIf(expr));
		ArrayList<String> ifExpr = (ArrayList<String>) expr;
		if (ifExpr.size() != 4) {
			throw new EvalError("Bad if expression: " + expr.toString());
		}
		if (!meval(ifExpr.get(1), env).equals(false)) {
			return meval(ifExpr.get(2),env);
		}
		else {
			return meval(ifExpr.get(3),env);
		}
	}	
	@SuppressWarnings("unchecked")
	public static void evalDefinition(Object expr, Environment env) throws EvalError {
		assert(isDefinition(expr));
		ArrayList<String> def;
		try {
			def = (ArrayList<String>) expr;
			if (def.size() != 3) {
				throw new EvalError("Bad definition " + expr.toString());
			}
			String name = def.get(1);
			Object value = meval(def.get(2),env);
			env.addVariable(name, value);
		}
		catch (Exception e){
			throw new EvalError("Bad definition " + expr.toString());
		}
		return;
	}
	public static Object evalName(Object expr, Environment env) throws EvalError {
		assert(isName(expr));
		return env.lookupVariable((String)expr);
	}
	@SuppressWarnings("unchecked")
	public static Object evalApplication(Object expr, Environment env) throws EvalError {	
		ArrayList<Object> subexprs = (ArrayList<Object>) expr;		
		ArrayList<Object> subexprvals = new ArrayList<Object>();
		for (int i=0; i<subexprs.size(); i++) {
			subexprvals.add(meval(subexprs.get(i),env));
		}		
		return mapply(subexprvals.get(0), new ArrayList<Object>(subexprvals.subList(1,subexprvals.size())));
	}
	public static Object mapply(Object proc, ArrayList<Object> operands) throws EvalError {
		if (isPrimitiveProcedure(proc)) {		
			String procName = (String) proc;
			if (procName.equals("+")) {
				return Primitives.primitivePlus(operands);
			}
			if (procName.equals("-")) {
				return Primitives.primitiveMinus(operands);
			}
			if (procName.equals("*")) {
				return Primitives.primitiveTimes(operands);
			}
			if (procName.equals("=")) {
				return Primitives.primitiveEquals(operands);
			}
			if (procName.equals("cons")) {
				Object temp = Primitives.primitiveCons(operands);
				return temp;
			}
			if (procName.equals("car")) {
				return Primitives.primitiveCar(operands);
			}
			if (procName.equals("cdr")) {
				return Primitives.primitiveCdr(operands);
			}
			if (procName.equals("zero?")) {
				return Primitives.primitiveZero(operands);
			}
			if (procName.equals("null?")) {
				return Primitives.primitiveNullTest(operands);
			}
			if (procName.equals("list")) {
				return Primitives.primitiveList(operands);
			}
			if (procName.equals("<")) {
				return Primitives.primitiveLessThan(operands);
			}
			if (procName.equals(">")) {
				return Primitives.primitiveGreater(operands);
			}
			if (procName.equals("<=")) {
				return Primitives.primitiveLessThanEquals(operands);
			}
		}
		else if (proc instanceof Procedure) {
			Procedure pProc = (Procedure) proc;
			ArrayList<Object> params = pProc.getParams();
			Environment newenv = new Environment(pProc.getEnvironment());
			if (params.size() != operands.size()) {
				throw new EvalError("Parameter length mismatch: " + proc.toString() + " given operands " + operands.toString());
			}
			for (int i=0; i<operands.size(); i++) {
				newenv.addVariable((String) params.get(i), operands.get(i));
			}
			return meval(pProc.getBody(),newenv);
		}
		throw new EvalError("Application of non-procedure: " + proc.toString());
	}
	@SuppressWarnings("unchecked")
	public static Object evalCond(Object expr, Environment env) throws EvalError {
		assert(isCond(expr));
		ArrayList<Object> condList = ((ArrayList<Object>) expr);	
		for (int i=1; i<condList.size(); i++) {
			if (!(condList.get(i) instanceof ArrayList<?>)) {
				throw new EvalError("Bad cond expression: " + condList.get(i).toString());
			}
			ArrayList<Object> pairing = ((ArrayList<Object>) condList.get(i));
			if (pairing.size() != 2) {
				throw new EvalError("Bad cond expression: " + condList.get(i).toString());
			}
			if (meval(pairing.get(0),env) != Boolean.FALSE) {
				return meval(pairing.get(1),env);
			}
		}
		return "";
	}
	@SuppressWarnings("unchecked")
	public static Object evalLambda(Object expr, Environment env) throws EvalError {
		assert(isLambda(expr));
		ArrayList<Object> lambdaExpr = (ArrayList<Object>) expr;
		if (lambdaExpr.size() != 3) {
			throw new EvalError("Bad lambda expression: " + expr.toString());
		}
		return new Procedure(((ArrayList<Object>)lambdaExpr.get(1)),((ArrayList<Object>)lambdaExpr.get(2)),env);
	}
}